🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / core / network / src / commonTest / kotlin / org / meshtastic / core / network / radio / ReplayFuzzTest.kt
Displaying Raw • Download
core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/ReplayFuzzTest.kt bf929c20f274232e0eb8ddc19b2dc82403e99e5b (bf929c20) Text, 6.15 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.network.radio
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787org.meshtastic.core.repository.HandshakeConstants
Tff7b72import T7ee787org.meshtastic.core.repository.RadioTransportCallback
Tff7b72import T7ee787org.meshtastic.core.repository.TransportDisconnectReason
Tff7b72import T7ee787org.meshtastic.proto.FromRadio
Tff7b72import T7ee787org.meshtastic.proto.MeshPacket
Tff7b72import T7ee787org.meshtastic.proto.MyNodeInfo
Tff7b72import T7ee787org.meshtastic.proto.NodeInfo
Tff7b72import T7ee787org.meshtastic.proto.ToRadio
Tff7b72import T7ee787kotlin.random.Random
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertTrue
T8b949e/**
* Seeded fuzz suite for the replay ingestion path, built on [ReplayFuzz]. Each test states an invariant the
* untrusted-input boundary must hold and sweeps it over [ReplayFuzz.DEFAULT_ITERATIONS] deterministic seeds; a failure
* message carries the seed so it reproduces locally. These run in `allTests`, so the parse/decode boundary is fuzzed on
* every CI run at no app-size cost.
*
* Layers covered here are the self-contained ones with clean pass/fail oracles: the asset parser, the handshake input,
* and the protobuf decoder. The post-decode handler chain (the layer that is *not* exception-isolated) needs the full
* `MeshMessageProcessor` graph and a richer oracle — the [ReplayFuzz.adversarialFromRadio] corpus is built to feed that
* integration harness when it lands.
*/
Tff7b72class T56d364ReplayFuzzTest Tb4b4b4{
T8b949e/** Collects relayed frames without decoding, so the oracle is the transport's behaviour, not the callback's. */
Tff7b72private Tff7b72class T56d364Sink Tb4b4b4: Te6edf3RadioTransportCallback Tb4b4b4{
Tff7b72val Te6edf3frames Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3ByteArrayTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffonConnectTb4b4b4(Tb4b4b4) Tff7b72= Tffa657Unit
Tff7b72override Tff7b72fun Td2a8ffonDisconnectTb4b4b4(Te6edf3isPermanentTb4b4b4: Tffa657BooleanTb4b4b4, Te6edf3errorMessageTb4b4b4: Tffa657String?Tb4b4b4, Te6edf3reasonTb4b4b4: Te6edf3TransportDisconnectReason?Tb4b4b4) Tff7b72=
Tffa657Unit
Tff7b72override Tff7b72fun Td2a8ffhandleFromRadioTb4b4b4(Te6edf3bytesTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tb4b4b4{
Te6edf3frames Tff7b72+Tff7b72= Te6edf3bytes
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3sampleConfig Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3FromRadioTb4b4b4(Te6edf3my_info Tff7b72= Te6edf3MyNodeInfoTb4b4b4(Te6edf3my_node_num Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3sampleNodes Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3FromRadioTb4b4b4(Te6edf3node_info Tff7b72= Te6edf3NodeInfoTb4b4b4(Te6edf3num Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3samplePackets Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3FromRadioTb4b4b4(Te6edf3packet Tff7b72= Te6edf3MeshPacketTb4b4b4(Te6edf3id Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffvalidAssetTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3ReplayFuzzTb4b4b4.Te6edf3assetTb4b4b4(Te6edf3sampleConfigTb4b4b4, Te6edf3sampleNodesTb4b4b4, Te6edf3samplePacketsTb4b4b4)
T8b949e/**
* A corrupt asset must fail fast and *only* as [IllegalArgumentException] — never OOM, hang, or leak okio errors.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`corrupt assets fail only with IllegalArgumentException`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3ReplayFuzzTb4b4b4.Te6edf3forSeeds Tb4b4b4{ Te6edf3randomTb4b4b4, Te6edf3seed Tff7b72-Tff7b72>
Tff7b72val Te6edf3input Tff7b72= Tff7b72if Tb4b4b4(Te6edf3seed Tff7b72% T79c0ff2 Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Te6edf3ReplayFuzzTb4b4b4.Te6edf3mutateTb4b4b4(Te6edf3randomTb4b4b4, Te6edf3validAssetTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72else Te6edf3ReplayFuzzTb4b4b4.Te6edf3randomBytesTb4b4b4(Te6edf3randomTb4b4b4)
Tff7b72val Te6edf3error Tff7b72=
Te6edf3runCatching Tb4b4b4{ Te6edf3ReplayRadioTransportTb4b4b4(Te6edf3SinkTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72thisTb4b4b4, Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff"Tb4b4b4, Te6edf3frames Tff7b72= Te6edf3inputTb4b4b4, Te6edf3packetDelayMs Tff7b72= T79c0ff0Tb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3error Tff7b72=Tff7b72= Tff7b72null Tff7b72|Tff7b72| Te6edf3error Tff7b72is Te6edf3IllegalArgumentExceptionTb4b4b4,
Ta5d6ff"Ta5d6ffseed=Tffd700$Te6edf3seedTa5d6ff: parser raised Tffd700${Te6edf3errorTff7b72?.Te6edf3let Tb4b4b4{ Tffa657itTff7b72::Te6edf3classTb4b4b4.Te6edf3simpleName Tb4b4b4}Tffd700}Ta5d6ff: Tffd700${Te6edf3errorTff7b72?.Te6edf3messageTffd700}Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/** The transport must absorb any outbound bytes — undecodable [ToRadio] is dropped, not thrown. */
Tf0883e@Test
Tff7b72fun Td2a8ff`handleSendToRadio tolerates arbitrary outbound bytes`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3transport Tff7b72= Te6edf3ReplayRadioTransportTb4b4b4(Te6edf3SinkTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72thisTb4b4b4, Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff"Tb4b4b4, Te6edf3frames Tff7b72= Te6edf3validAssetTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3packetDelayMs Tff7b72= T79c0ff0Tb4b4b4)
Te6edf3ReplayFuzzTb4b4b4.Te6edf3forSeeds Tb4b4b4{ Te6edf3randomTb4b4b4, Te6edf3seed Tff7b72-Tff7b72>
Tff7b72val Te6edf3error Tff7b72=
Te6edf3runCatching Tb4b4b4{
Te6edf3transportTb4b4b4.Te6edf3handleSendToRadioTb4b4b4(Te6edf3ReplayFuzzTb4b4b4.Te6edf3mutateTb4b4b4(Te6edf3randomTb4b4b4, Te6edf3ToRadioTb4b4b4(Te6edf3want_config_id Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3encodeTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3handleSendToRadioTb4b4b4(Te6edf3ReplayFuzzTb4b4b4.Te6edf3randomBytesTb4b4b4(Te6edf3randomTb4b4b4)Tb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3error Tff7b72=Tff7b72= Tff7b72nullTb4b4b4, Ta5d6ff"Ta5d6ffseed=Tffd700$Te6edf3seedTa5d6ff: handleSendToRadio threw Tffd700$Te6edf3errorTa5d6ff"Tb4b4b4)
Tb4b4b4}
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e/**
* Decoding a bit-flipped frame may throw, but only a catchable [Exception] — an [Error] (OOM/SOE) would be a DoS.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`decode of bit-flipped frames never raises an Error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3seedFrame Tff7b72= Te6edf3FromRadioTb4b4b4(Te6edf3node_info Tff7b72= Te6edf3NodeInfoTb4b4b4(Te6edf3num Tff7b72= T79c0ff7Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3encodeTb4b4b4(Tb4b4b4)
Te6edf3ReplayFuzzTb4b4b4.Te6edf3forSeeds Tb4b4b4{ Te6edf3randomTb4b4b4, Te6edf3seed Tff7b72-Tff7b72>
Tff7b72val Te6edf3mutated Tff7b72= Te6edf3ReplayFuzzTb4b4b4.Te6edf3mutateTb4b4b4(Te6edf3randomTb4b4b4, Te6edf3seedFrameTb4b4b4)
Tff7b72val Te6edf3error Tff7b72= Te6edf3runCatching Tb4b4b4{ Te6edf3FromRadioTb4b4b4.Te6edf3ADAPTERTb4b4b4.Te6edf3decodeTb4b4b4(Te6edf3mutatedTb4b4b4) Tb4b4b4}Tb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3error Tff7b72=Tff7b72= Tff7b72null Tff7b72|Tff7b72| Te6edf3error Tff7b72is Te6edf3ExceptionTb4b4b4, Ta5d6ff"Ta5d6ffseed=Tffd700$Te6edf3seedTa5d6ff: decode raised Tffd700$Te6edf3errorTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/** Hostile-but-valid content must replay through the transport intact (every relayed frame still decodes). */
Tf0883e@Test
Tff7b72fun Td2a8ff`adversarial frames replay through the transport without crashing it`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3sink Tff7b72= Te6edf3SinkTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3nodes Tff7b72= Tb4b4b4(T79c0ff0 Te6edf3until T79c0ff2T79c0ff5Tb4b4b4)Tb4b4b4.Te6edf3map Tb4b4b4{ Te6edf3FromRadioTb4b4b4(Te6edf3node_info Tff7b72= Te6edf3ReplayFuzzTb4b4b4.Te6edf3adversarialNodeTb4b4b4(Te6edf3RandomTb4b4b4(Tffa657itTb4b4b4)Tb4b4b4)Tb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3packets Tff7b72= Tb4b4b4(T79c0ff0 Te6edf3until T79c0ff7T79c0ff5Tb4b4b4)Tb4b4b4.Te6edf3map Tb4b4b4{ Te6edf3FromRadioTb4b4b4(Te6edf3packet Tff7b72= Te6edf3ReplayFuzzTb4b4b4.Te6edf3adversarialPacketTb4b4b4(Te6edf3RandomTb4b4b4(Tffa657itTb4b4b4)Tb4b4b4)Tb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3transport Tff7b72=
Te6edf3ReplayRadioTransportTb4b4b4(
Te6edf3callback Tff7b72= Te6edf3sinkTb4b4b4,
Te6edf3scope Tff7b72= Tff7b72thisTb4b4b4,
Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ff"Tb4b4b4,
Te6edf3frames Tff7b72= Te6edf3ReplayFuzzTb4b4b4.Te6edf3assetTb4b4b4(Te6edf3sampleConfigTb4b4b4, Te6edf3nodesTb4b4b4, Te6edf3packetsTb4b4b4)Tb4b4b4,
Te6edf3packetDelayMs Tff7b72= T79c0ff0Tb4b4b4,
Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3handleSendToRadioTb4b4b4(Te6edf3ToRadioTb4b4b4(Te6edf3want_config_id Tff7b72= Te6edf3HandshakeConstantsTb4b4b4.Te6edf3NODE_INFO_NONCETb4b4b4)Tb4b4b4.Te6edf3encodeTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3sinkTb4b4b4.Te6edf3framesTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3sinkTb4b4b4.Te6edf3framesTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3FromRadioTb4b4b4.Te6edf3ADAPTERTb4b4b4.Te6edf3decodeTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4} T8b949e// round-trip held under hostile field values
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.06s